<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>策略模式</title>
</head>
<body>
<script>
    /**
     * 策略模式：定义一系列的算法，把它们一个个封装起来，并且使它们可互相替换
     */

    // 根据绩效计算奖金，奖金为基本工资*绩效等级
    function calculateBound(performanceLevel, salary) {
        if (performanceLevel === 'S') {
            return salary * 4
        }

        if (performanceLevel === 'A') {
            return salary * 3
        }

        if (performanceLevel === 'B') {
            return salary * 2
        }
    }

    calculateBound('S', 20000)

    // 这段代码的问题是违反了开放-封闭原则、算法复用性差
    // 算法复用的问题可通过将其封装到一个个小函数中来解决
    function performanceS(salary) {
        return salary * 4
    }

    function performanceA(salary) {
        return salary * 3
    }

    function performanceB(salary) {
        return salary * 2
    }

    function calculateBound(performanceLevel, salary) {
        if (performanceLevel === 'S') {
            return performanceS(salary)
        }

        if (performanceLevel === 'A') {
            return performanceA(salary)
        }

        if (performanceLevel === 'B') {
            return performanceB(salary)
        }
    }


    /**
     * 这种改善十分有限，最大的问题是函数可能越来越大，在系统变化时缺乏弹性，可使用策略模式来重构
     * 策略模式由两部分组成：策略类和环境类Context
     * 策略类封装了具体算法，负责具体的计算过程
     * 环境类用于接受客户的请求，然后将请求委托给某个策略类
     */

    // 基于传统OOP实现的策略模式
    function PerformanceS() {

    }

    PerformanceS.prototype.calculate = function (salary) {
        return salary * 4
    }

    function PerformanceA() {

    }

    PerformanceA.prototype.calculate = function (salary) {
        return salary * 3
    }

    function PerformanceB() {

    }

    PerformanceB.prototype.calculate = function (salary) {
        return salary * 2
    }

    function Bonus() {
        this.salary = null // 原始工资
        this.strategy = null // 绩效等级对应的策略对象
    }

    // 设置员工的基本工资
    Bonus.prototype.setSalary = function (salary) {
        this.salary = salary
    }

    // 设置员工绩效等级对应的策略对象
    Bonus.prototype.setStrategy = function (strategy) {
        this.strategy = strategy
    }

    // 把计算奖金的操作委托给对应的策略对象
    Bonus.prototype.getBounds = function () {
        return this.strategy.calculate(this.salary)
    }

    const bonus = new Bonus()

    bonus.setSalary(10000)
    bonus.setStrategy(new PerformanceS())


    /**
     * JS版本的策略模式
     * JS中函数也是对象，Context没必要用类来表示
     */
    const strategies = {
        S: function (salary) {
            return salary * 4
        },
        A: function (salary) {
            return salary * 3
        },
        B: function (salary) {
            return salary * 2
        }
    }

    function calculateBonus(level, salary) {
        return strategies[level](salary)
    }

    calculateBonus('S', 20000)

    /**
     * 策略模式不仅可封装算法，如果一些业务规则指向的目标一致
     * 也可用来封装一系列的业务规则，如表单校验
     */

    /*
        版本一，不使用策略模式的平常代码，缺点：
        1.缺乏弹性，添加新规则则需要深入函数内部来修改，违反开放-封闭原则
        2.算法复用性差，如果另一表单也有类似校验，则逻辑需要复制粘贴
     */
    const registerForm = document.getElementById('registerForm')

    registerForm.onsubmit = function () {
        if (registerForm.userName.value = '') {
            alert('用户名不能为空')
            return false
        }
        if (registerForm.password.value.length < 6) {
            alert('密码长度不能少于6位')
            return false
        }
        if (!/^1[3|5|8][0-9]{9}$/.test(registerForm.phoneNumber.value)) {
            alert('手机号码格式不正确')
            return false
        }
    }

    /*
        使用策略模式重构
        Validator类在这里作为Context，负责接收用户请求并委托给strategy对象
        使用策略模式后，仅仅通过“配置”的方式就可完成一个表单的校验
        这些校验也可复用在程序的任何地方

        策略模式的优点是避免了很多if分支语句，符合开放-封闭原则
        算法的封装使得它们易于切换、理解和扩展
        缺点是必须要了解所有的策略才能选择一个合适的策略，所以策略模式需要暴露它的所有实现
        这违反了最小知识原则

        除了使用类来封装算法和行为，在JS这种将函数作为一等对象的语言中，策略模式已融入到了语言本身
        可用高阶函数来封装不同行为，并把它传递到另一函数中，我们对这些函数发出“调用”消息时，不同函数
        会返回不同的执行结果
     */
    const strategies = {
        isNonEmpty: function (value, errorMsg) {
            if (value === '') return errorMsg
        },
        minLength: function (value, length, errorMsg) {
            if (value.length < length) return errorMsg
        },
        isMobile: function (value, errorMsg) {
            if (!/^1[3|5|8][0-9]{9}$/.test(value)) return errorMsg
        }
    }

    function Validator() {
        this.cache = [] // 保存校验规则
    }

    Validator.prototype.add = function (dom, rule, errorMsg) {
        const arg = rule.split(':')
        const validFn = function () {
            const strategy = arg.shift()
            arg.unshift(dom.value)
            arg.push(errorMsg)
            return strategies[strategy].apply(dom, arg)
        }
        this.cache.push(validFn)
    }
    Validator.prototype.start = function () {
        for (let i = 0, validatorFn; validatorFn = this.cache[i++];) {
            const msg = validatorFn()

            if (msg) return msg
        }
    }

    function validateFn() {
        const validator = new Validator()

        // 添加校验规则
        validator.add(registerForm.userName, 'isNonEmpty', '用户名不能为空')
        validator.add(registerForm.password, 'minLength:6', '密码长度不能少于6位')
        validator.add(registerForm.phoneNumber, 'isMobile', '手机号码格式不正确')

        // 获取校验结果
        const errorMsg = validator.start()

        return errorMsg
    }

    registerForm.onsubmit = function () {
        const errorMsg = validateFn()

        if (errorMsg) {
            alert(errorMsg)
            return false
        }
    }

    // 如果要为某个文本输入框添加多条校验规则
    function validateFn() {
        const validator = new Validator()

        // 添加校验规则
        validator.add(registerForm.userName, [
            {strategy: 'isNonEmpty', errorMsg: '用户名不能为空'},
            {strategy: 'minLength:6', errorMsg: '用户名长度不能小于6位'},
        ])

        // 获取校验结果
        const errorMsg = validator.start()

        return errorMsg
    }

    Validator.prototype.add = function (dom, rules) {
        for (let i = 0, rule; rule = rules[i++];) {
            const strategyArg = rule.strategy.split(':')
            const errorMsg = rule.errorMsg
            const validFn = function () {
                const strategy = strategyArg.shift()
                strategyArg.unshift(dom.value)
                strategyArg.push(errorMsg)
                return strategies[strategy].apply(dom, strategyArg)
            }

            this.cache.push(validFn)
        }
    }

</script>
</body>
</html>